#!/usr/bin/env python3

import os
import argparse
from typing import Set, List, Optional

try:
    import pathspec
except ImportError:
    print("Error: Missing dependency 'pathspec'. Install it with: pip install pathspec")
    exit(1)

# Default code file extensions to scan if the --ext argument is not provided.
DEFAULT_EXTENSIONS = [
    # C/C++
    '.c', '.h', '.cpp', '.hpp', '.cxx', '.hxx',
    # Python
    '.py', '.pyw',
    # Java
    '.java',
    # JavaScript / TypeScript
    '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx',
    # Web
    '.html', '.htm', '.css', '.scss', '.sass',
    # Shell
    '.sh', '.bash', '.zsh',
    # Go
    '.go',
    # Rust
    '.rs',
    # Ruby
    '.rb',
    # PHP
    '.php',
    # Swift
    '.swift',
    # Kotlin
    '.kt', '.kts',
    # C#
    '.cs',
    # Config/Data
    '.json', '.yaml', '.yml', '.toml', '.xml', '.ini', '.md'
]


def count_lines_in_file(file_path: str) -> int:
    """Counts the total number of lines in a single file. Returns -1 if the file cannot be read."""
    try:
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            return sum(1 for _ in f)
    except Exception as e:
        print(f"Warning: Could not read '{file_path}': {e}")
        return -1


def should_exclude(path: str, exclude_spec: Optional[pathspec.PathSpec]) -> bool:
    """Check if a path should be excluded by pathspec rules."""
    if exclude_spec is None:
        return False
    # pathspec expects posix-style paths
    rel_path = os.path.relpath(path).replace(os.sep, '/')
    return exclude_spec.match_file(rel_path)


def scan_path(path: str, extensions: Set[str], verbose: bool, exclude_spec: Optional[pathspec.PathSpec]) -> (int, int):
    """Scans a file or directory and returns (line_count, file_count)."""
    total_lines = 0
    total_files = 0

    if os.path.isfile(path):
        if should_exclude(path, exclude_spec):
            if verbose:
                print(f"Excluded file: {path}")
            return 0, 0
        if os.path.splitext(path)[1].lower() in extensions:
            lines = count_lines_in_file(path)
            if lines >= 0:
                if verbose:
                    print(f"{path}: {lines} lines")
                total_lines = lines
                total_files = 1
    elif os.path.isdir(path):
        for root, dirs, files in os.walk(path):
            # 目录也要检查是否被排除
            if should_exclude(root, exclude_spec):
                if verbose:
                    print(f"Excluded dir: {root}")
                dirs[:] = []  # 跳过整个目录
                continue

            for filename in files:
                file_path = os.path.join(root, filename)
                if should_exclude(file_path, exclude_spec):
                    if verbose:
                        print(f"Excluded file: {file_path}")
                    continue
                if os.path.splitext(filename)[1].lower() in extensions:
                    lines_in_file = count_lines_in_file(file_path)
                    if lines_in_file >= 0:
                        if verbose:
                            print(f"{file_path}: {lines_in_file} lines")
                        total_lines += lines_in_file
                        total_files += 1
    else:
        print(f"Error: Path '{path}' does not exist or is not valid.")

    return total_lines, total_files


def main():
    parser = argparse.ArgumentParser(
        description="Counts total lines in files within directories or single files. Supports exclusions (gitignore-style).",
        epilog="Examples:\n"
               "  cspider.py src/ tests/ -x '*.md' '*.json'\n"
               "  cspider.py . -v -x 'build/' 'third_party/'\n",
        formatter_class=argparse.RawTextHelpFormatter
    )

    parser.add_argument(
        'paths',
        nargs='*',
        default=['.'],
        help='One or more directories or files to scan (defaults to current directory).'
    )
    parser.add_argument(
        '--ext',
        nargs='+',
        help='Specify file extensions to scan (e.g., .py .js .html)'
    )
    parser.add_argument(
        '-v', '--verbose',
        action='store_true',
        help='Print each file and line count.'
    )
    parser.add_argument(
        '-x', '--exclude',
        nargs='+',
        help='Exclude files/dirs matching gitignore-style patterns (supports multiple).'
    )

    args = parser.parse_args()

    target_paths = args.paths
    if args.ext:
        target_extensions = {ext.lower() if ext.startswith('.') else '.' + ext.lower() for ext in args.ext}
    else:
        target_extensions = set(DEFAULT_EXTENSIONS)

    # Compile exclude spec
    exclude_spec = None
    if args.exclude:
        exclude_spec = pathspec.PathSpec.from_lines('gitwildmatch', args.exclude)

    print(f"🚀 Starting scan...")
    abs_paths = [os.path.abspath(p) for p in target_paths]
    print(f"Scanning Paths: {', '.join(abs_paths)}")
    print(f"File Extensions: {' '.join(sorted(list(target_extensions)))}")
    if args.exclude:
        print(f"Exclude Patterns: {' '.join(args.exclude)}")
    if args.verbose:
        print("Verbose mode enabled.")
    print("-" * 40)

    grand_total_lines = 0
    grand_total_files = 0
    for path in target_paths:
        lines, files = scan_path(path, target_extensions, args.verbose, exclude_spec)
        grand_total_lines += lines
        grand_total_files += files

    if args.verbose and grand_total_files > 0:
        print("-" * 40)

    if grand_total_files > 0:
        print(f"🎉 Scan complete!")
        print(f"Total Files Scanned: {grand_total_files}")
        print(f"Total Lines of Code: {grand_total_lines}")
    else:
        print("🤔 No matching files were found or files were empty.")


if __name__ == '__main__':
    main()
